<?php
namespace Lib\Http;

/**
 * HTTP 表单管理器。
 *
 * @author SUSISOY
 *
 */
class Form {
	public $type = 'application/x-www-form-urlencoded';
	
	/**
	 * 常见文件扩展名对应的 MIME 类型串。
	 * @var array
	 */
	private $_mime = array('jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'js' => 'application/javascript', 'json' => 'application/json', 'html' => 'text/html', 'css' => 'text/css', 'txt' => 'text/plain', 'log' => 'text/plain', 'xml' => 'application/xml', 'pdf' => 'application/pdf', 'rar' => 'application/x-rar-compressed', 'zip' => 'application/zip', 'bz' => 'application/x-bzip', 'bz2' => 'application/x-bzip2', 'tar' => 'application/x-tar', 'mp3' => 'audio/mpeg', 'mp4' => 'video/mp4');
	/**
	 * 当前表单中的变量名称列表。
	 * @var array
	 */
	private $_name = [];
	/**
	 * 当前表单中的变量值列表。
	 * @var array
	 */
	private $_value = [];
	/**
	 * 当前表单中的待上传文件列表。
	 * @var array
	 */
	private $_files = [];
	/**
	 * multipart/form-data 类型数据中，各部分的分隔符。
	 * @var string
	 */
	private $_boundary = '';

	/**
	 * 初始化一个 Form 对象。
	 *
	 * @return void
	 */
	public function __construct() {
		$this->_boundary = '----------HHO-LIB-HTTP-BOUNDARY-' . strtoupper(uniqid());
	}

	/**
	 * 添加或修改一个或多个变量，若变量值为数组，则保持数组数据结构，典型示例：
	 * + self::set('N', 'V');
	 * + self::set('N', array('V'));
	 * + self::set(array('N1'=>'V1', 'N2'=>'V2'));
	 * + self::set(array('N1'=>'V1', 'N2'=>array('V2')));
	 * + self::set(array('N1'=>array('V1'), 'N2'=>array('V2')));
	 *
	 * - 注意：$replace 为 TRUE 时，若存在同名变量，则覆盖；当打算提交一个数组时，$replace 应为 FALSE。
	 *
	 * @param string|array $name
	 * @param mixed $value
	 * @param boolean $replace
	 * @return boolean
	 */
	public function set($name, $value = '', $replace = TRUE) {
		if (!$name) {
			return FALSE;
		}
		
		$vars = [];
		
		if (is_scalar($name)) {
			$vars[$name] = $value;
		} else {
			foreach ($name as $n => $v) {
				$vars[$n] = $v;
			}
		}
		
		foreach ($vars as $k => $v) {
			if ($replace) {
				$offset = array_search($k, $this->_name);
				
				if ($offset === FALSE) {
					$offset = count($this->_name);
				}
			} else {
				$offset = count($this->_name);
			}
			
			$this->_name[$offset] = $k;
			$this->_value[$offset] = $v;
		}
		
		return TRUE;
	}

	/**
	 * 参见 self::set() 方法。
	 *
	 * @param string $name
	 * @param mixed $value
	 * @return void
	 */
	public function __set($name, $value) {
		$this->set($name, $value);
	}

	/**
	 * 获得一个或多个变量的值，若指定变量不存在，则返回 NULL，典型示例：
	 * + self::get();
	 * + self::get('N');
	 * + self::get(array('N1','N2'));
	 *
	 * - 注意：获取一个变量时，返回值可能是一个简单类型值，但有多个同名变量时，返回值将是一个数组。
	 *
	 * @param string|array $name
	 * @return string|array|NULL
	 */
	public function get($name = NULL) {
		$values = [];
		
		if (is_null($name)) {
			foreach ($this->_name as $offset => $value) {
				if (isset($values[$value])) {
					is_array($values[$value]) || $values[$value] = [$values[$value]];
					
					$values[$value][] = $this->_value[$offset];
				} else {
					$values[$value] = $this->_value[$offset];
				}
			}
			
			return $values;
		}
		
		$offsets = [];
		
		if (is_scalar($name)) {
			$offsets[$name] = array_keys($this->_name, $name);
		} else {
			foreach ($name as $n) {
				$offsets[$n] = array_keys($this->_name, $n);
			}
		}
		
		foreach ($offsets as $key => $value) {
			if (!$value) {
				continue;
			}
			
			$x = array_intersect_key($this->_value, array_flip($value));
			switch (count($x)) {
				case 0 :
					break;
				case 1 :
					$values[$key] = current($x);
					break;
				default :
					$values[$key] = $x;
			}
		}
		
		switch (count($values)) {
			case 0 :
				return NULL;
			case 1 :
				return current($values);
		}
		
		return $values;
	}

	/**
	 * 参见 self::get() 方法。
	 *
	 * @param string $name
	 * @return string
	 */
	public function __get($name) {
		return $this->get($name);
	}

	/**
	 * 删除一个或多个变量，典型示例：
	 * + self::del('N');
	 * + self::del(array('N1','N2'));
	 *
	 * @param string|array $name
	 * @return boolean
	 */
	public function del($name) {
		if (!$name) {
			return FALSE;
		}
		
		if (is_scalar($name)) {
			$keys = array_keys($this->_name, $name);
		} else {
			$keys = [];
			
			foreach ($name as $n) {
				$keys = array_merge($keys, array_keys($this->_name, $n));
			}
		}
		
		$this->_name = array_diff_key($this->_name, array_flip($keys));
		$this->_value = array_diff_key($this->_value, array_flip($keys));
		
		return TRUE;
	}

	/**
	 * 删除一个变量，参见 self::del() 方法。
	 *
	 * @param string $name
	 * @return void
	 */
	public function __unset($name) {
		$this->del($name);
	}

	/**
	 * 检查一个变量是否存在。
	 *
	 * @param string $name
	 * @return boolean
	 */
	public function exists($name) {
		return in_array($name, $this->_name);
	}

	/**
	 * 参见 self::exists() 方法。
	 *
	 * @param string $name
	 * @return boolean
	 */
	public function __isset($name) {
		return $this->exists($name);
	}

	/**
	 * POST 方式提交数据，将所有变量进行 application/x-www-form-urlencoded 编码并返回。
	 *
	 * @return string
	 */
	public function urlencode() {
		$values = [];
		
		foreach ($this->_name as $offset => $name) {
			$values[] = urlencode($name) . '=' . urlencode($this->_value[$offset]);
		}
		
		return implode('&', $values);
	}

	/**
	 * 解析一条 application/x-www-form-urlencoded 编码串，并将解析所得变量添加至表单中
	 *
	 * @param string $data
	 * @return int
	 */
	public function urldecode($data) {
		$count = 0;
		
		foreach (explode('&', $data) as $var) {
			if (preg_match('/^([^\=]+)\=([^\=]*)$/', $var, $x)) {
				$this->set(urldecode($x[1]), urldecode($x[2]), FALSE) && $count++;
			}
		}
		
		return $count;
	}

	/**
	 * GET 方式提交数据，将所有变量进行 RFC 3986（PHP 5.3.0 之前为 RFC 1738）编码并返回。
	 *
	 * @return string
	 */
	public function rawurlencode() {
		return str_ireplace(array('+', '%7E'), array('%20', '~'), $this->urlencode());
	}

	/**
	 * 解析一条  RFC 3986（PHP 5.3.0 之前为 RFC 1738）编码串，并将解析所得变量添加至表单中
	 *
	 * @param string $data
	 * @return int
	 */
	public function rawurldecode($data) {
		return $this->urldecode($data);
	}

	/**
	 * 添加一个待上传的文件，变量名可以为一维数组形式。
	 *
	 * @param string $var
	 * @param string $filename
	 * @param mixed $data
	 * @return boolean
	 */
	public function addFile($var, $filename, $data = NULL) {
		if (is_null($data) && !is_file($filename)) {
			return FALSE;
		}
		
		$files = array('var' => $var, 'files' => $filename);
		
		is_null($data) || $files['data'] = $data;
		
		$this->_files[] = $files;
		
		$this->type == 'application/x-www-form-urlencoded' && $this->type = 'multipart/form-data; boundary=' . $this->_boundary;
		
		return TRUE;
	}

	/**
	 * 将指定文件列表从上传文件列表中删除。
	 *
	 * @param string $var
	 * @return boolean
	 */
	public function delFile($var) {
		foreach ($this->_files as $k => $v) {
			if ($v['var'] == $var) {
				unset($this->_files[$k]);
			}
		}
		
		count($this->_files) == 0 && $this->type = 'application/x-www-form-urlencoded';
		
		return TRUE;
	}

	/**
	 * 获得所有待上传的文件，其中包括变量名，文件名描述，文件名，文件 MIME 类型和文件数据，其数据结构如下：
	 * - array('var'=>'..', 'files'=>'..', 'name'=>'..', 'type'=>'...', 'data'=>'...')
	 *
	 * @return array
	 */
	public function getFile() {
		$files = [];
		
		foreach ($this->_files as $file) {
			$info = pathinfo($file['files']);
			$info['extension'] = isset($info['extension']) ? strtolower($info['extension']) : '';
			
			$file['name'] = $info['basename'];
			$file['type'] = isset($this->_mime[$info['extension']]) ? $this->_mime[$info['extension']] : 'application/octet-stream';
			
			$files[] = $file;
		}
		
		return $files;
	}

	/**
	 * 获得当前表单的 HTTP 消息体数据。
	 *
	 * - $full 参数指定是否获得完整的 HTTP 消息体。在上传文件且 $full=FALSE 时将节约大量内存资源。
	 *
	 * @param boolean $full
	 * @return string|array
	 */
	public function body($full = TRUE) {
		if (empty($this->_files)) {
			$body = $this->urlencode();
			
			return $full ? $body : array(array('data' => $body, 'size' => strlen($body)));
		}
		
		if ($full) {
			$body = '';
			
			foreach ($this->_name as $offset => $name) {
				$body .= "--$this->_boundary\r\n";
				$body .= "Content-Disposition: form-data; name=\"{$name}\"\r\n";
				$body .= "\r\n";
				$body .= $this->_value[$offset] . "\r\n";
			}
			
			foreach ($this->_files as &$file) {
				$info = pathinfo($file['files']);
				$info['extension'] = isset($info['extension']) ? strtolower($info['extension']) : '';
				
				$body .= "--$this->_boundary\r\n";
				$body .= "Content-Disposition: form-data; name=\"{$file['var']}\"; filename=\"{$info['basename']}\"\r\n";
				$body .= 'Content-Type: ' . (isset($this->_mime[$info['extension']]) ? $this->_mime[$info['extension']] : 'application/octet-stream') . "\r\n";
				$body .= "\r\n";
				$body .= (isset($file['data']) ? $file['data'] : file_get_contents($file['files'])) . "\r\n";
			}
			
			$body .= "--$this->_boundary--\r\n";
		} else {
			$key = 0;
			$body = [];
			
			foreach ($this->_name as $offset => $name) {
				$body[$key]['data'] = "--$this->_boundary\r\n";
				$body[$key]['data'] .= "Content-Disposition: form-data; name=\"{$name}\"\r\n";
				$body[$key]['data'] .= "\r\n";
				$body[$key]['data'] .= $this->_value[$offset] . "\r\n";
				
				$body[$key]['size'] = strlen($body[$key]['data']);
				
				$key++;
			}
			
			foreach ($this->_files as &$file) {
				$info = pathinfo($file['files']);
				$info['extension'] = isset($info['extension']) ? strtolower($info['extension']) : '';
				
				$body[$key]['data'] = "--$this->_boundary\r\n";
				$body[$key]['data'] .= "Content-Disposition: form-data; name=\"{$file['var']}\"; filename=\"{$info['basename']}\"\r\n";
				$body[$key]['data'] .= 'Content-Type: ' . (isset($this->_mime[$info['extension']]) ? $this->_mime[$info['extension']] : 'application/octet-stream') . "\r\n";
				$body[$key]['data'] .= "\r\n";
				
				if (isset($file['data'])) {
					$body[$key]['data'] .= $file['data'] . "\r\n";
					
					$body[$key]['size'] = strlen($body[$key]['data']);
				} else {
					$body[$key]['files'] = $file['files'];
					
					$body[$key]['size'] = strlen($body[$key]['data']) + filesize($file['files']) + 2;
				}
				
				$key++;
			}
			
			$body[$key]['data'] = "--$this->_boundary--\r\n";
			$body[$key]['size'] = strlen($body[$key]['data']);
		}
		
		return $body;
	}
}